﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO.Packaging;
using System.IO;

namespace Ins.Packaging
{

    /// <summary>
    /// Provides packaging functionalities.
    /// </summary>
    public class Pack
    {
        private const long BUFFER_SIZE = 4096;

        private Package _pkg;
        private string _packagePath;

        private PackageSettings _settings;

        /// <summary>
        /// Initializes a new instance of the Pack object, based on a package file.
        /// </summary>
        /// <param name="packagePath">The full path of the package file.</param>
        public Pack(string packagePath)
        {
            _packagePath = packagePath;
            _pkg = Package.Open(_packagePath, FileMode.Open);
            _settings = new PackageSettings();
        }

        /// <summary>
        /// Initializes a new instance of the Pack object, based on a package file and specified settings.
        /// </summary>
        /// <param name="packagePath">The full path of the package file.</param>
        /// <param name="settings">A package settings object containing package namespaces, relation types, etc.</param>
        public Pack(string packagePath, PackageSettings settings) : this(packagePath)
        {
            _settings = settings;
        }

        /// <summary>
        /// Initializes a new instance of the Pack object, based an existing Package object.
        /// </summary>
        /// <param name="pkg">A Package object.</param>
        public Pack(Package pkg)
        {
            _pkg = pkg;
            _settings = new PackageSettings();
        }

        /// <summary>
        /// Initializes a new instance of the Pack object, based on a package object and specified settings.
        /// </summary>
        /// <param name="pkg">A Package object.</param>
        /// <param name="settings">A package settings object containing package namespaces, relation types, etc.</param>
        public Pack(Package pkg, PackageSettings settings)
            : this(pkg)
        {
            _settings = settings;
        }

        /// <summary>
        /// Adds a file to the specified Zip file name.
        /// </summary>
        /// <param name="zipFilename">The Zip file name.</param>
        /// <param name="subFolderName">Sub-folder inside the Zip to store the newly added file.</param>
        /// <param name="fileToAdd">The full path of the file to be added to the Zip.</param>
        public static void AddFileToZip(string zipFilename, string subFolderName, string fileToAdd)
        {
            using (Package zip = Package.Open(zipFilename, FileMode.OpenOrCreate))
            {
                string destFileName;
                if (!string.IsNullOrEmpty(subFolderName))
                    destFileName = ".\\" + subFolderName + "\\" + Path.GetFileName(fileToAdd);
                else
                    destFileName = ".\\" + Path.GetFileName(fileToAdd);
                Uri uri = PackUriHelper.CreatePartUri(new Uri(destFileName, UriKind.Relative));
                if (zip.PartExists(uri))
                {
                    zip.DeletePart(uri);
                }
                PackagePart part = zip.CreatePart(uri, "", CompressionOption.Normal);
                using (FileStream fileStream = new FileStream(fileToAdd, FileMode.Open, FileAccess.Read))
                {
                    using (Stream dest = part.GetStream())
                    {
                        CopyStream(fileStream, dest);
                    }
                }
            }
        }

        /// <summary>
        /// Extracts the content of a package file.
        /// </summary>
        /// <param name="packageFilePath">The full path of the package file.</param>
        /// <returns></returns>
        public static List<string> Extract(string packageFilePath)
        {
            List<string> fileNames = new List<string>();
            using (Package package = Package.Open(
                packageFilePath, FileMode.Open, FileAccess.Read))
            {
                FileInfo fi = new FileInfo(packageFilePath);
                string newPath, relativePath, fileName;
                foreach (PackagePart part in package.GetParts())
                {
                    fileName = part.Uri.OriginalString.Substring(part.Uri.OriginalString.LastIndexOf("/") + 1);
                    relativePath = part.Uri.OriginalString.Substring(0, part.Uri.OriginalString.LastIndexOf("/"));
                    if (relativePath.StartsWith("/")) relativePath = relativePath.Substring(1);
                    newPath = Path.Combine(fi.DirectoryName, relativePath);
                    Directory.CreateDirectory(newPath);
                    string target = Path.Combine(
                        newPath, fileName);
                    using (Stream source = part.GetStream(
                        FileMode.Open, FileAccess.Read))
                    using (Stream destination = File.OpenWrite(target))
                    {
                        byte[] buffer = new byte[0x1000];
                        int read;
                        while ((read = source.Read(buffer, 0, buffer.Length)) > 0)
                        {
                            destination.Write(buffer, 0, read);
                        }
                        fileNames.Add(target);
                    }
                }
            }
            return fileNames;
        }

        /// <summary>
        /// Gets the document part from the current package object.
        /// </summary>
        /// <returns>Returns a PackagePart object representing the document.</returns>
        /// <remarks>
        /// The document part is required to retrieve the package parts and the relationships.
        /// </remarks>
        public PackagePart GetDocumentPart()
        {
            PackagePart documentPart = null;

            Uri uriDocumentTarget = null;
            foreach (PackageRelationship relationship in
                _pkg.GetRelationshipsByType(_settings.PackageRelationshipType))
            {
               uriDocumentTarget = PackUriHelper.ResolvePartUri(
                    new Uri("/", UriKind.Relative), relationship.TargetUri);

                documentPart = _pkg.GetPart(uriDocumentTarget);
                break;
            }
            return documentPart;
        }

        /// <summary>
        /// Deletes the specified part from the package.
        /// </summary>
        /// <param name="documentPart">The document part object required to access the part.</param>
        /// <param name="relationshipType">The relationship type, usually represented by a Xml namespace, between the part and the document.</param>
        /// <param name="partPath">The absolute path of the part.</param>
        /// <returns>True if the deletion was successful.</returns>
        /// <remarks>
        /// The method does not delete the relationship. This is because we use this method to replace a part, not to permanently delete it.
        /// </remarks>
        private bool DeletePart(PackagePart documentPart, string relationshipType, string partPath)
        {
            PackagePart part = GetPart(documentPart, relationshipType, partPath);
            if (part != null)
            {
                _pkg.DeletePart(part.Uri);
                //var relList = GetPartRelationships(documentPart, relationshipType, partPath);
                //foreach (var rel in relList)
                //{
                //    documentPart.DeleteRelationship(rel.Id);
                //    _pkg.DeleteRelationship(rel.Id);
                //}
                return true;
            }
            return false;
        }

        /// <summary>
        /// Returns the first matching part relationship based on the specified relationship type.
        /// </summary>
        /// <param name="documentPart">The document part object required to retrieve the relationship.</param>
        /// <param name="relationshipType">The relationship type, usually specified by a Xml namespace.</param>
        /// <param name="partPath">The absolute path of the part in the package.</param>
        /// <returns>A PackageRelationship object representing the relation or null if not found.</returns>
        /// <remarks>
        /// The part relationship matching is based on the TargetUri property of the PackageRelationship object.
        /// </remarks>
        public PackageRelationship GetPartRelationship(PackagePart documentPart, string relationshipType, string partPath)
        {
            PackageRelationship rel = null;
            foreach (PackageRelationship relationship in documentPart.GetRelationshipsByType(relationshipType))
            {
                if (relationship.TargetUri.OriginalString.Equals(partPath, StringComparison.InvariantCultureIgnoreCase))
                {
                    rel = relationship;
                    break;
                }
            }
            return rel;
        }

        /// <summary>
        /// Returns a collection of relationships based on the specified relationship type.
        /// </summary>
        /// <param name="documentPart">The document part object required to retrieve the relationships.</param>
        /// <param name="relationshipType">The relationship type, usually a Xml namespace.</param>
        /// <param name="partPath">The absolute path of the part in the package.</param>
        /// <returns>An IEnumerable of PackageRelationship objects.</returns>
        public IEnumerable<PackageRelationship> GetPartRelationships(PackagePart documentPart, string relationshipType, string partPath)
        {
            List<PackageRelationship> list = new List<PackageRelationship>();
            foreach (PackageRelationship relationship in documentPart.GetRelationshipsByType(relationshipType))
            {
                if (relationship.TargetUri.OriginalString.Equals(partPath, StringComparison.InvariantCultureIgnoreCase))
                {
                    list.Add(relationship);
                    break;
                }
            }
            return list;
        }

        /// <summary>
        /// Creates a part in the current Package object, based on the part relationship type, part path, content type, and the part source file.
        /// </summary>
        /// <param name="relationshipType">The relationship type, usually a Xml namespace.</param>
        /// <param name="partPath">The absolute path of the part inside the package.</param>
        /// <param name="contentType">The part content type. Can be </param>
        /// <param name="sourceFileName">The full path of the file containing the source data for the newly created part.</param>
        /// <remarks>This method does not create the part relationship. It assumes the part relationship already exists, because it is used for parts replacment.</remarks>
        private void CreatePart(string relationshipType, string partPath, string contentType, string sourceFileName)
        {
            Uri partUri = PackUriHelper.CreatePartUri(new Uri(partPath, UriKind.Relative));
            //_pkg.CreateRelationship(documentPart.Uri, TargetMode.Internal, relationshipType);

            PackagePart part = _pkg.CreatePart(partUri, contentType);

            using (FileStream fileStream = new FileStream(sourceFileName, FileMode.Open, FileAccess.Read))
            {
                CopyStream(fileStream, part.GetStream());
            }
            //documentPart.CreateRelationship(new Uri(partPath, UriKind.Relative), TargetMode.Internal, relationshipType);
        }

        /// <summary>
        /// Replaces one existing part in the package with a new part based on a source file.
        /// </summary>
        /// <param name="documentPart">The document part required to delete the existing part.</param>
        /// <param name="relationshipType">The part relationship type, usually a Xml namepsace.</param>
        /// <param name="partPath">The absolute path of the part in the package.</param>
        /// <param name="contentType">The content type of the newly created part.</param>
        /// <param name="sourceFileName">The full path of the source file.</param>
        /// <remarks>
        /// This method calls DeletePart and CreatePart internal methods to update the package by replacing one part.
        /// </remarks>
        public void ReplacePart(PackagePart documentPart, string relationshipType, string partPath, string contentType, string sourceFileName)
        {
            DeletePart(documentPart, relationshipType, partPath);
            CreatePart(relationshipType, partPath, contentType, sourceFileName);
        }

        /// <summary>
        /// Returns a part object located at the specified path, based on a relationship type.
        /// </summary>
        /// <param name="documentPart">The document part required to retrieve the part object.</param>
        /// <param name="relationshipType">The relationship type, usually a Xml namespace.</param>
        /// <param name="partPath">The absolute path of the part in the package.</param>
        /// <returns>A PackagePart object.</returns>
        /// <remarks>
        /// The method loops through all the corresponding nrelations and checks for the file name (not the full path).
        /// The relationship match is based on the TargetUri property.
        /// </remarks>
        public PackagePart GetPart(PackagePart documentPart, string relationshipType, string partPath)
        {
            Uri partUri = null;
            PackagePart part = null;
            string relTargetFileName, partFileName;
            foreach (PackageRelationship relationship in
                documentPart.GetRelationshipsByType(relationshipType))
            {
                relTargetFileName = Path.GetFileName(relationship.TargetUri.OriginalString);
                partFileName = Path.GetFileName(partPath);
                if(relTargetFileName.Equals(partFileName, StringComparison.InvariantCultureIgnoreCase))
                {
                    partUri = PackUriHelper.ResolvePartUri(
                        documentPart.Uri, relationship.TargetUri);

                    part = _pkg.GetPart(partUri);
                    break;
                }
            }
            return part;
        }

        /// <summary>
        /// Extracts the specified part to a nominated directory.
        /// </summary>
        /// <param name="part">The part to be extracted.</param>
        /// <param name="targetDirectory">The directory to extract the part into.</param>
        public static void ExtractPart(PackagePart part, string targetDirectory)
        {
            string stringPart = part.Uri.ToString().TrimStart('/');
            Uri partUri = new Uri(stringPart, UriKind.Relative);

            Uri uriFullPartPath =
            new Uri(new Uri(targetDirectory, UriKind.Absolute), partUri);

            Directory.CreateDirectory(
                Path.GetDirectoryName(uriFullPartPath.LocalPath));

            using (FileStream fileStream =
                new FileStream(uriFullPartPath.LocalPath, FileMode.Create))
            {
                CopyStream(part.GetStream(), fileStream);
            }
        }

        /// <summary>
        /// Copies the raw data from one stream to another.
        /// </summary>
        /// <param name="inputStream">The input (source) stream.</param>
        /// <param name="outputStream">The output (destination) stream.</param>
        private static void CopyStream(System.IO.Stream inputStream, System.IO.Stream outputStream)
        {
            long bufferSize = inputStream.Length < BUFFER_SIZE ? inputStream.Length : BUFFER_SIZE;
            byte[] buffer = new byte[bufferSize];
            int bytesRead = 0;
            long bytesWritten = 0;
            while ((bytesRead = inputStream.Read(buffer, 0, buffer.Length)) != 0)
            {
                outputStream.Write(buffer, 0, bytesRead);
                bytesWritten += bufferSize;
            }
        }

    }
}


